package com.example.mq.mqserver.datacenter.Impl;

import com.example.mq.common.BinaryTool;
import com.example.mq.mqserver.core.MSGQueue;
import com.example.mq.mqserver.core.Message;
import com.example.mq.mqserver.datacenter.IMessageFileManager;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Scanner;

/**
 * 通过这个类把消息持久化到文件
 * 消息使用逻辑删除，因此持久化消息需要两个文件，1：消息本体 2：统计文件
 */
public class MessageFileManagerImpl implements IMessageFileManager {
    //  使用一个内部类来表示统计文件
    static class Stat {
        int totalCount;
        int validCount;
        public Stat(int totalCount,int validCount) {
            this.totalCount = totalCount;
            this.validCount = validCount;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Stat stat = (Stat) o;
            return totalCount == stat.totalCount && validCount == stat.validCount;
        }
        @Override
        public int hashCode() {
            return Objects.hash(totalCount, validCount);
        }
    }

    @Override
    public void init() {}

    //  获取队列文件路径
    private String getQueueDirPath(String queueName) {
        return "./data/" + queueName;
    }
    //  获取统计文件路径
    private String getQueueStatPath(String queueName) {
        return getQueueDirPath(queueName) + "/queue_stat.txt";
    }
    //  获取消息文件路径
    private String getQueueDataPath(String queueName) {
        return getQueueDirPath(queueName) + "/queue_data.txt";
    }

    @Override
    public void createQueueFiles(String queueName) throws IOException {
        File fileDir = new File(getQueueDirPath(queueName));
        if(!fileDir.exists()) {
            boolean ok = fileDir.mkdirs();
            if(!ok) {
                throw new IOException("创建队列目录失败! file:" + fileDir.getAbsolutePath());
            }
        }
        File queueDataFile = new File(getQueueDataPath(queueName));
        if(!queueDataFile.exists()) {
            boolean ok = queueDataFile.createNewFile();
            if(!ok) {
                throw new IOException("创建队列数据文件失败! file:" + queueDataFile.getAbsolutePath());
            }
        }
        File queueStatFile = new File(getQueueStatPath(queueName));
        if(!queueStatFile.exists()) {
            boolean ok = queueStatFile.createNewFile();
            if(!ok) {
                throw new IOException("创建队列统计文件失败! file:" + queueStatFile.getAbsolutePath());
            }
            //  给消息文件赋初值
            Stat stat = new Stat(0,0);
            writeStat(queueName,stat);
        }
    }
    private void writeStat(String queueName, Stat stat) {
        try(OutputStream outputStream = Files.newOutputStream(Paths.get(getQueueStatPath(queueName)))) {
            //  使用 PrintWriter 字符流写入文件
            //  使用 PrintWriter 的好处是每次都是覆盖写
            try(PrintWriter printWriter = new PrintWriter(outputStream)) {
                printWriter.write(stat.totalCount + "\t" + stat.validCount);
                printWriter.flush();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    private Stat readStat(String queueName) {
        try(InputStream inputStream = Files.newInputStream(Paths.get(getQueueStatPath(queueName)))) {
            //  使用 Scanner 读取文本文件
            Scanner scanner = new Scanner(inputStream);
            Stat stat = new Stat(0,0);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();

            return stat;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void destroyFiles(String queueName) throws IOException {
        File queueDataFile = new File(getQueueDataPath(queueName));
        boolean ok1 = queueDataFile.delete();
        File queueStatFile = new File(getQueueStatPath(queueName));
        boolean ok2 = queueStatFile.delete();
        File queueDir = new File(getQueueDirPath(queueName));
        boolean ok3 = queueDir.delete();
        if(!ok1 || !ok2 || !ok3) {
            throw new IOException("删除队列文件失败!");
        }
    }

    @Override
    public void sendMessage(MSGQueue queue, Message message) throws IOException {
        //  1. 检查队列文件是否存在
        if(!checkFilesExists(queue.getName())) {
            throw new IOException("对应文件不存在! ");
        }

        //  2. 序列化数据
        byte[] data = BinaryTool.toBytes(message);

        synchronized (queue) {
            //  3. 把消息持久化到文件
            File dataFile = new File(getQueueDataPath(queue.getName()));
            //  3.1 计算当前消息的偏移量（要先留出4字节保存数据的长度）
            long fileLength = dataFile.length();
            message.setOffsetBeg(fileLength + 4);
            message.setOffsetEnd(fileLength + 4 + data.length);
            //  3.2 写入文件
            //  第二个参数不能少，设置为true，表示追加写，不传参，默认覆盖写
            try(OutputStream outputStream = new FileOutputStream(getQueueDataPath(queue.getName()),true)) {
                //  此处要写多字节数据，使用 DataOutputStream
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    //  先写消息体的长度,再写消息本体
                    dataOutputStream.writeInt(data.length);
                    dataOutputStream.write(data);
                }
            }
            //  4. 更新统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount += 1;
            stat.validCount += 1;
            writeStat(queue.getName(),stat);
        }

    }
    //  检查数据文件和统计文件是否都存在
    private boolean checkFilesExists(String queueName) {
        File dataFile = new File(getQueueDataPath(queueName));
        File StatFile = new File(getQueueStatPath(queueName));
        return dataFile.exists() && StatFile.exists();
    }

    @Override
    public void deleteMessage(MSGQueue queue,Message message) throws IOException, ClassNotFoundException {
        //  1. 检查文件是否存在
        if(!checkFilesExists(queue.getName())) {
            throw new IOException("对应文件不存在! ");
        }

        synchronized (queue) {
            //  2. 逻辑删除消息
            try(RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()),"rw")) {
                //  2.1 开辟一个与目标消息一样大的数组
                long msgOffsetBeg = message.getOffsetBeg();
                long msgOffsetEnd = message.getOffsetEnd();
                byte[] curData = new byte[(int) (msgOffsetEnd - msgOffsetBeg)];
                //  2.2 将文件光标移动到消息初始位置,并读取数据
                randomAccessFile.seek(msgOffsetBeg);
                randomAccessFile.read(curData);
                //  2.3 将数组反序列化,并修改字段
                Message diskMessage = (Message) BinaryTool.fromBytes(curData);
                diskMessage.setIsValid((byte) 0);
                //  2.4 重新把消息写入文件
                byte[] newBytes = BinaryTool.toBytes(diskMessage);
                randomAccessFile.seek(msgOffsetBeg);
                randomAccessFile.write(newBytes);
            }

            //  3. 更新统计文件
            Stat stat = readStat(queue.getName());
            if(stat.validCount > 0) {
                stat.validCount -= 1;
            }
            writeStat(queue.getName(),stat);
        }
    }

    @Override
    public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, ClassNotFoundException {
        //  1. 检查对应文件是否存在
        if(!checkFilesExists(queueName)) {
            throw new IOException("对应文件不存在! ");
        }
        LinkedList<Message> messageList = new LinkedList<>();

        //  2. 从硬盘中读取所有消息
        try(InputStream inputStream = Files.newInputStream(Paths.get(getQueueDataPath(queueName)))) {
            try(DataInputStream dataInputStream = new DataInputStream(inputStream)) {
                long msgOffsetBeg = 0;
                while(true) {
                    //  2.1 先读取消息长度
                    int messageSize = dataInputStream.readInt();
                    //  2.2 开辟数组，读取消息
                    byte[] buffer = new byte[messageSize];
                    int read = dataInputStream.read(buffer);//  dataInputStream 读取数据后，文件光标会自动向后走
                    if(read != messageSize) {
                        throw new IOException("数据文件格式错误! ");
                    }
                    //  2.3 反序列化，并检查当前的消息是否有效
                    Message message = (Message) BinaryTool.fromBytes(buffer);
                    if(message.getIsValid() != 0x1) {
                        msgOffsetBeg += (4 + messageSize);
                        continue;
                    }
                    //  2.4 设置属性,并加入链表
                    message.setOffsetBeg(msgOffsetBeg + 4);
                    message.setOffsetEnd(msgOffsetBeg + 4 + messageSize);
                    messageList.add(message);
                    //  2.5 重新计算偏移量
                    msgOffsetBeg += (4 + messageSize);
                }
            }
            catch (EOFException e) {
                //  EOFException 是 DataInputStream 读取完数据后的提示（不像其他流可以用-1表示）
                System.out.println("数据加载完成.");
            }
        }
        return  messageList;
    }

    @Override
    public void queueGC(MSGQueue queue) throws IOException, ClassNotFoundException {
        //  使用复制算法进行垃圾回收
        synchronized (queue) {
            //  1. 获取到所有有效的消息
            LinkedList<Message> messages = loadAllMessageFromQueue(queue.getName());

            //  2. 创建一个新文件
            String newDataFilePath = getQueueDirPath(queue.getName()) + "/queue_data_new.txt";
            File newDataFile = new File(newDataFilePath);
            if(newDataFile.exists()) {
                //  文件已存在，说明上次 GC 不完整
                throw new IOException("GC 发生错误! queue_data_new.txt 已存在.\n" + newDataFile.getAbsolutePath());
            }
            boolean ok = newDataFile.createNewFile();
            if(!ok) {
                throw new IOException("GC 发生错误! 创建新文件失败.");
            }

            //  3. 删除旧文件
            File dataFile = new File(getQueueDataPath(queue.getName()));
            ok = dataFile.delete();
            if(!ok) {
                throw new IOException("GC 发生错误! 删除旧文件失败. file:" + dataFile.getAbsolutePath());
            }

            //  4. 重命名文件
            //  此时磁盘的文件虽然没了，但内存中 dataFile 值不变
            ok = newDataFile.renameTo(dataFile);
            if(!ok) {
                throw new IOException("GC 发生错误! 文件重命名失败.");
            }

            //  5. 把有效的消息重新写入新文件
            //  先删文件再写消息的好处是可以重新计算消息的下标
            for (Message message : messages) {
                sendMessage(queue,message);
            }

            //  6. 更新统计文件
            int totalCount = messages.size();
            Stat stat = new Stat(totalCount, totalCount);
            writeStat(queue.getName(),stat);

        }

    }

    @Override
    public boolean checkGC(String queueName) {
        //  规定 消息总数>100 并且 有效消息少于1/2时 触发GC
        Stat stat = readStat(queueName);
        int totalCount = stat.totalCount;
        int validCount = stat.validCount;
        if(totalCount > 100 && (double)validCount / (double)totalCount < 0.5) {
            return true;
        }
        return false;
    }

}